Quickstart
Get one real result out of Bowmark before you install anything, then wire it into your agent.
Most integrations ask you to install first and trust that something happened. Bowmark is a plain HTTP endpoint behind an API key, so you can do it the other way round: get a real answer out of it first, decide it is worth installing second.
1 — See it work. Nothing installed.
Every call needs a Bowmark account. Sign up at
bowmark.ai/sign-up, create a key at
bowmark.ai/dashboard/keys, and export it as
BOWMARK_API_KEY. Every account gets $10 of usage free each month (Pricing). Then paste this into any terminal:
curl -s https://api.bowmark.ai/v1/run \
-H "Authorization: Bearer $BOWMARK_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"script":"const { offers } = await bowmark.pcparts.search(\"RTX 5080\"); return offers.slice(0, 3);"}'It sits there for about ten seconds, because in that time Bowmark is searching Newegg, Micro Center and B&H at once and price-sorting what comes back. This is what it printed, recorded 2026-08-24:
{
"ok": true,
"status": "ok",
"ms": 9953,
"result": [
{
"store": "newegg",
"price": 1449.99,
"title": "MSI INSPIRE GeForce RTX 5080 16GB GDDR7 PCI Express 5.0 Graphics Card"
},
{
"store": "bhphoto",
"price": 1499.99,
"title": "Gigabyte GeForce RTX 5080 MASTER Graphics Card"
},
{
"store": "newegg",
"price": 1549.99,
"title": "PNY GeForce RTX 5080 OC 16GB 256-Bit GDDR7 DLSS 4.0 Graphics Card"
}
]
}Your prices will differ, and that is the point: those numbers came off the retailers' live pages while the request was open, not out of an index.
Using Python? Set the User-Agent header
The curl example above works out of the box. If you translate it to Python using
stdlib urllib.request, you'll get a 403 Forbidden (error code 1010) because
Python's default User-Agent header is blocked by our WAF. Fix it with one line:
headers = {
"Authorization": f"Bearer {os.environ['BOWMARK_API_KEY']}",
"Content-Type": "application/json",
"User-Agent": "my-app/1.0", # ← add this line
}If you use requests, httpx, aiohttp, or the bowmark-web package, you're not
affected — their User-Agent strings are fine. Full details and a working example are in
Scripting.
What the ten seconds were spent on
Every response also carries a trace. It is the receipt — one row per site Bowmark
actually reached, with what each one gave back. Summarised, from the same run:
newegg ok 3 offers 4100ms
bhphoto ok 3 offers 5426ms
microcenter empty 0 offers 9943ms
-> pcparts ok 9946msThree retailers were searched in parallel and Micro Center had nothing matching, so the answer is the cheapest of two. Bowmark reports that rather than hiding it — which is the difference between a result you can quote and one you can only hope about.
That was the whole product
You just wrote a script against the capability library and had it executed against live retail sites. No browser was opened, no page was rendered, and nothing was installed. Everything below is about getting your agent to do that on its own.
2 — Install it into your agent
One line, one time. Pick your host. Where a plugin exists, take it — it wires the MCP and installs the skill that teaches your agent when to reach for Bowmark, and it lands in the host's own plugin list. Installation has the config for every host, including the double-click bundle for Claude Desktop.
claude mcp add bowmark --transport http https://api.bowmark.ai/mcp \
--header "Authorization: Bearer $BOWMARK_API_KEY"Run it from any terminal. Every project picks Bowmark up — there is no per-project step, and nothing to name in a prompt. Claude reaches for it on its own when a task needs the live web.
Want the skill bundled in too? One install does both:
claude plugin marketplace add bowmark-ai/plugin
claude plugin install bowmark@bowmark-aiFour clicks, about a minute. Full screenshots are on Installation.
Go to chatgpt.com/plugins and click + (top right).
Name it Bowmark, choose Server URL, paste https://api.bowmark.ai/mcp/chatgpt-app,
set OAuth, tick the box, hit Create.
Hit Connect on the next screen and sign in to Bowmark.
In a chat, type @ and pick Bowmark. You must do this every turn — see the warning below.
Settings → MCP → Add new MCP server, then paste:
{
"mcpServers": {
"bowmark": {
"url": "https://api.bowmark.ai/mcp",
"headers": { "Authorization": "Bearer <your Bowmark API key>" }
}
}
}codex plugin marketplace add bowmark-ai/pluginThen codex /plugins and install Bowmark from the list. Or edit
~/.codex/config.toml directly:
[mcp_servers.bowmark]
url = "https://api.bowmark.ai/mcp"
bearer_token_env_var = "BOWMARK_API_KEY"Install the SDK into your Node or Python project:
Node.js:
npm install @bowmark/webPython:
pip install bowmark-webThen use the typed SDK in your code:
Node.js:
import { Bowmark } from "@bowmark/web";
const bowmark = new Bowmark({ apiKey: process.env.BOWMARK_API_KEY });
const { offers } = await bowmark.pcparts.search("RTX 5080");
console.log(offers);Python:
from bowmark import Bowmark
bowmark = Bowmark(api_key=os.environ["BOWMARK_API_KEY"])
offers = bowmark.pcparts.search("RTX 5080")
print(offers)For more details and examples, see Installation and Scripting.
Same URL, same key in the Authorization header. Windsurf, Claude Desktop, LM Studio,
browser-use, your own agent — every path is on Installation.
If your host speaks stdio rather than HTTP, bridge it:
{
"mcpServers": {
"bowmark": {
"command": "npx",
"args": ["@bowmark/mcp"],
"env": { "BOWMARK_API_KEY": "<your Bowmark API key>" }
}
}
}3 — Give your agent its first task
Paste this into a fresh chat, verbatim:
Price an RTX 5080 across every retailer you can reach, and tell me
which store is cheapest and what it costs right now.In ChatGPT, type @ and pick Bowmark first. Everywhere else, just send it.
A working agent does three things, in this order, and you can watch each one:
- Calls
get_librarywith something like"pc parts". No site is touched. - Calls
runwith a few lines of JavaScript it wrote against what came back. - Answers with a store name, a price, and a link you can click.
If it answers without calling a tool, it did not use Bowmark — it answered from
memory or browsed. In ChatGPT that is almost always the missing @.
4 — When it doesn't work
Three failures account for nearly all of them.
The agent never called Bowmark
In ChatGPT, a custom connector is inert until you attach it for that turn. This is
documented OpenAI behaviour and no amount of prompt wording changes it. Type @,
pick Bowmark, then ask. Everywhere else, the tools are in scope on every turn.
TypeError: Cannot read properties of undefined
The script called a function with the wrong argument shape and then read a field off
undefined. Signatures are not uniform on purpose — bowmark.pcparts.search takes a
plain string, bowmark.flights.search takes an object.
// wrong — search takes a string here
const { offers } = await bowmark.pcparts.search({ query: "RTX 5080" });
// right
const { offers } = await bowmark.pcparts.search("RTX 5080");The fix is always the same: read the library for that capability and check the signature. It costs nothing and touches no site.
How you read it depends on which channel you are on. In an agent it is the
get_library tool. Over HTTP — including if you are POSTing to /v1/run as above — it is
a second endpoint, never something you can call from inside the script:
curl -s "https://api.bowmark.ai/v1/library?query=pc%20parts" \
-H "Authorization: Bearer $BOWMARK_API_KEY"It answered, but the answer looks thin
Read warnings and the trace together — they say different things, and reading
only one of them is how a thin answer gets quoted as a complete one.
warningsnames every site dropped from the fan-out: it failed, or it timed out. A populatedwarningsmeans a site was never heard from at all.tracehas one row per site regardless, with what each one returned. A row readingemptyis a site that answered and had nothing matching.
So an empty warnings does not mean every site had a result — it means none were
dropped. The run above is exactly that case: warnings was [], and Micro Center
still contributed nothing. Read both before you call an answer complete.
Check status before ok — partial means the script ran and the result is real
but narrower than you asked for. Full rules in Scripting.
The site I need isn't supported
If the library comes back empty for the site or capability you're looking for, there's no dedicated provider for it yet. That doesn't mean Bowmark can't reach it.
Call bowmark.read.page(url) (or read.pages for several URLs at once) through run()
— it fetches any page as a plain GET and transparently escalates to a real browser only if
the page needs one. This is Bowmark's own answer to "the site isn't in the list", and it
works for most pages with no dedicated provider at all. session() and the top-level
bowmark client both refuse it (code: "rung_undeclared" — its rung is decided per
call, not statically), so run() is not optional here. See How it works § No provider?
Read any page directly for the
full example.
If a page genuinely needs more than that — it's behind a login, needs a click or a date picked on a widget before the data shows up, or you want it turned into a typed capability rather than raw HTML:
- Reach for
bowmark.browser_agentif the blocker is interaction, not access — a form to fill, a calendar widget to click through, a wizard to drive. It's Bowmark's own hosted browser agent, built for exactly the caseread.pagecan't handle. It always pauses for a human at a watch link when a login is needed, and that is true on every run — there is no way to store or replay a login across runs, even against the same site twice. See Scripting § When nothing else works: the browser agent. - Email support@bowmark.ai with the site name and what you were trying to do. Bowmark prioritizes sites by actual usage and by direct request. Your request is tracked as demand — every time an agent reaches for a site we don't cover, we see it and add it to the priority queue.
- Keep going anyway. Your agent can still reach the site via a browser or a direct API call if one exists. Bowmark works alongside whatever your agent does, not instead of it.
Write actions and multi-step flows with browser_agent
Everything above reads data. Bowmark also runs a hosted browser agent for sites that need interaction — filling forms, clicking through wizards, signing in, any action a person would perform in their browser.
When to use browser_agent
- The site needs interaction you can't script: a form to fill, a date to pick on a calendar widget, a wizard to step through
- You're writing data: form submissions, order placement, profile updates, content uploads
- The site needs multiple steps in sequence: login, navigate, fill form, verify, submit
- Standard reads won't work because the data only appears after you interact with the page
If the site just needs a login and then serves you regular pages, you don't need browser_agent — use the SDK or a capability directly with a caller-supplied login. browser_agent is for the interaction itself.
The async polling pattern
browser_agent runs asynchronously. You start it in one run(), then poll for status from later runs. You cannot start it and immediately wait for the result in the same run — that would block forever and hit the 90-second timeout.
// Step 1: Start the browser agent (in one run)
const { result: started } = await run(
`return bowmark.browser_agent.start({
task: "Fill out the form on athenahealth.com with patient ID 12345, add clinical note, and submit"
})`
);
const { id, watchUrl } = started;
console.log("Browser agent started. Watch at:", watchUrl);// Step 2: Poll from a LATER run (or multiple later runs)
// You can do this immediately, or wait and try again later
for (let i = 0; i < 10; i++) {
const { result: status } = await run(
`return bowmark.browser_agent.status(${JSON.stringify(id)}, { waitMs: 60000 })`
);
console.log("Status:", status.status);
if (status.status === "needs_input") {
// The browser agent needs you to answer something (login, captcha, etc)
// Use browser_agent.send() to provide input
break;
}
if (status.status === "idle") {
// Done! The result is in status.result
console.log("Completed with result:", status.result);
await run(`return bowmark.browser_agent.stop(${JSON.stringify(id)})`);
break;
}
// status.status === "running" means keep polling
}Understanding timeouts and polling
What waitMs means: status() blocks for up to that many milliseconds waiting for the agent to finish. If it doesn't finish in that time, it returns status: "running" and you can poll again.
waitMs: 60000(60 seconds) — recommended for most tasks. The polling call itself will wait this long before returning.waitMs: 5000(5 seconds) — faster feedback loop if you want to check on progress frequently- Default if you omit it — the endpoint waits as long as it can
The whole run() call times out at 90 seconds. This is the ceiling for run(), not specific to browser_agent. A script that starts the agent and then immediately calls status() in a loop will hit this ceiling on the second poll.
Typical task takes 1-4 polls (about 1-3 minutes total). A repeated step name showing up on back-to-back polls is normal — multi-step sites need several attempts before they resolve. Don't stop on a status plateau; keep polling until you see idle or needs_input.
// ❌ DON'T do this — the second run() call will fail
const start = await run(`return bowmark.browser_agent.start({task})`);
const status = await run(`return bowmark.browser_agent.status(start.result.id)`);
// This throws because status() inside a loop hits the 90s ceiling
// ✓ DO this — separate runs, so each gets its own 90s budget
const start = await run(`return bowmark.browser_agent.start({task})`);
// ... wait, then in a DIFFERENT call ...
const status = await run(`return bowmark.browser_agent.status(start.result.id)`);Success verification
The result field tells you what the browser agent found or did:
- For reads (checking information):
resultcontains the extracted data — text, values, confirmations - For writes (form submissions, uploads):
resultcontains a summary of what was written and any confirmation text from the site - Field-by-field verification: The summary describes what was completed. If you need to verify individual fields were filled correctly, include that in your original task description ("After filling the form, take a screenshot and list each field you filled")
status.result is a plain object; the structure depends on what you asked the agent to do:
const status = await run(`return bowmark.browser_agent.status(id)`);
// status.result might be:
// { message: "Form submitted successfully", confirmation_number: "ATH-123456" }
// or
// { order_placed: true, order_id: "12345", estimated_delivery: "2026-09-28" }Cost estimation
Each call to browser_agent costs $0.02–$0.25 per run, depending on task complexity and how long it takes. Typical tasks run 1-3 minutes and cost $0.02–$0.10.
See Pricing § Hosted browser agents for the full cost breakdown and how to estimate your costs at scale.
Unattended logins are not supported here
bowmark.secret() stores a credential for a typed provider's own signIn() call — it
is not a way to log in through browser_agent. start() takes a plain-language task
string and nothing else, and interpolating bowmark.secret("…") into that string sends
the browser agent the literal placeholder text, never the password, with no error. When
a task hits a login, browser_agent returns needs_input with kind: "takeover" and
waits for a person to act at watchUrl — every run, not just the first.
If the site has a typed provider, sign in once with providers.<site>.signIn() (using a
stored bowmark.secret()) and make repeated calls inside that session() instead. See
Scripting § When nothing else works: the browser agent
for the full explanation.
Stop when done
An open browser_agent session costs money until you stop it. Always call stop() when the task is done, even if the status was idle.
await run(`return bowmark.browser_agent.stop(${JSON.stringify(id)})`);Where to go next
- How it works — the two calls, drawn, and why the surface is a language rather than a menu of tools.
- Scripting — the globals, the sandbox rules, and how to read the response envelope. Includes a full reference for browser_agent.
- Installation — every host, with screenshots, plus API keys and what they unlock.
Reading this with an agent?